package com.asiainfo.simpledao;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class NameParameterSqlParser {

	private static final char[] PARAMETER_SEPARATORS = new char[] { '"', '\'', ':', '&', ',', ';', '(', ')', '|', '=', '+', '-', '*', '%', '/', '\\', '<', '>', '^' };
	private static final String[] START_SKIP = new String[] { "'", "\"", "--", "/*" };
	private static final String[] STOP_SKIP = new String[] { "'", "\"", "\n", "*/" };

	private String originalSql;
	private List<String> parameterNames = new ArrayList<String>();
	private List<int[]> parameterIndexes = new ArrayList<int[]>();
	private int namedParameterCount, totalParameterCount;

	public NameParameterSqlParser(final String sql) {
		if (sql == null || sql.length() == 0)
			throw new RuntimeException("SQL must not be null");
		Set<String> namedParameters = new HashSet<String>();
		String sqlToUse = sql;
		List<ParameterHolder> parameterList = new ArrayList<ParameterHolder>();
		char[] statement = sql.toCharArray();
		int escapes = 0;
		int i = 0;
		while (i < statement.length) {
			int skipToPosition = i;
			while (i < statement.length) {
				skipToPosition = skipCommentsAndQuotes(statement, i);
				if (i == skipToPosition) {
					break;
				} else {
					i = skipToPosition;
				}
			}
			if (i >= statement.length) {
				break;
			}
			char c = statement[i];
			if (c == ':' || c == '&') {
				int j = i + 1;
				if (j < statement.length && statement[j] == ':' && c == ':') {
					i = i + 2;
					continue;
				}
				String parameter = null;
				if (j < statement.length && c == ':' && statement[j] == '{') {
					while (j < statement.length && !('}' == statement[j])) {
						j++;
						if (':' == statement[j] || '{' == statement[j]) {
							throw new RuntimeException("Parameter name contains invalid character '" + statement[j] + "' at position " + i + " in statement: " + sql);
						}
					}
					if (j >= statement.length) {
						throw new RuntimeException("Non-terminated named parameter declaration at position " + i + " in statement: " + sql);
					}
					if (j - i > 3) {
						parameter = sql.substring(i + 2, j);
						namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter);
						totalParameterCount = addNamedParameter(parameterList, totalParameterCount, escapes, i, j + 1, parameter);
					}
					j++;
				} else {
					while (j < statement.length && !isParameterSeparator(statement[j])) {
						j++;
					}
					if (j - i > 1) {
						parameter = sql.substring(i + 1, j);
						namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter);
						totalParameterCount = addNamedParameter(parameterList, totalParameterCount, escapes, i, j, parameter);
					}
				}
				i = j - 1;
			} else {
				if (c == '\\') {
					int j = i + 1;
					if (j < statement.length && statement[j] == ':') {
						sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1);
						escapes++;
						i = i + 2;
						continue;
					}
				}
				if (c == '?') {
					totalParameterCount++;
				}
			}
			i++;
		}
		this.originalSql = sqlToUse;
		for (ParameterHolder ph : parameterList) {
			this.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
		}
	}

	private static int skipCommentsAndQuotes(char[] statement, int position) {
		for (int i = 0; i < START_SKIP.length; i++) {
			if (statement[position] == START_SKIP[i].charAt(0)) {
				boolean match = true;
				for (int j = 1; j < START_SKIP[i].length(); j++) {
					if (!(statement[position + j] == START_SKIP[i].charAt(j))) {
						match = false;
						break;
					}
				}
				if (match) {
					int offset = START_SKIP[i].length();
					for (int m = position + offset; m < statement.length; m++) {
						if (statement[m] == STOP_SKIP[i].charAt(0)) {
							boolean endMatch = true;
							int endPos = m;
							for (int n = 1; n < STOP_SKIP[i].length(); n++) {
								if (m + n >= statement.length) {
									return statement.length;
								}
								if (!(statement[m + n] == STOP_SKIP[i].charAt(n))) {
									endMatch = false;
									break;
								}
								endPos = m + n;
							}
							if (endMatch) {
								return endPos + 1;
							}
						}
					}
					return statement.length;
				}
			}
		}
		return position;
	}

	private static boolean isParameterSeparator(char c) {
		if (Character.isWhitespace(c)) {
			return true;
		}
		for (char separator : PARAMETER_SEPARATORS) {
			if (c == separator) {
				return true;
			}
		}
		return false;
	}

	private static int addNewNamedParameter(Set<String> namedParameters, int namedParameterCount, String parameter) {
		if (!namedParameters.contains(parameter)) {
			namedParameters.add(parameter);
			namedParameterCount++;
		}
		return namedParameterCount;
	}

	private static int addNamedParameter(List<ParameterHolder> parameterList, int totalParameterCount, int escapes, int i, int j, String parameter) {
		parameterList.add(new ParameterHolder(parameter, i - escapes, j - escapes));
		totalParameterCount++;
		return totalParameterCount;
	}

	private static class ParameterHolder {
		private final String parameterName;
		private final int startIndex;
		private final int endIndex;

		public ParameterHolder(String parameterName, int startIndex, int endIndex) {
			this.parameterName = parameterName;
			this.startIndex = startIndex;
			this.endIndex = endIndex;
		}

		public String getParameterName() {
			return this.parameterName;
		}

		public int getStartIndex() {
			return this.startIndex;
		}

		public int getEndIndex() {
			return this.endIndex;
		}
	}

	/**
	 * 替换 :name 为占位符 ?
	 * 
	 * @param parsedSql
	 * @return
	 */
	public String getUseSql() {
		String originalSql = getOriginalSql();
		StringBuilder actualSql = new StringBuilder();
		List<String> paramNames = getParameterNames();
		int lastIndex = 0;
		for (int i = 0; i < paramNames.size(); i++) {
			int[] indexes = getParameterIndexes(i);
			int startIndex = indexes[0];
			int endIndex = indexes[1];
			actualSql.append(originalSql, lastIndex, startIndex);
			actualSql.append("?");
			lastIndex = endIndex;
		}
		actualSql.append(originalSql, lastIndex, originalSql.length());
		return actualSql.toString();
	}

	private void addNamedParameter(String parameterName, int startIndex, int endIndex) {
		this.parameterNames.add(parameterName);
		this.parameterIndexes.add(new int[] { startIndex, endIndex });
	}

	public List<String> getParameterNames() {
		return this.parameterNames;
	}

	public int[] getParameterIndexes(int parameterPosition) {
		return this.parameterIndexes.get(parameterPosition);
	}

	public int getNamedParameterCount() {
		return this.namedParameterCount;
	}

	public int getTotalParameterCount() {
		return this.totalParameterCount;
	}

	public String getOriginalSql() {
		return this.originalSql;
	}

//	public Object[] parameters(Map<String, Object> params) {
//		List<String> parameterNames = getParameterNames();
//		List<Object> parameters = new ArrayList<Object>();
//		for (String parameterName : parameterNames) {
//			parameters.add(params.get(parameterName));
//		}
//		return parameters.toArray(new Object[0]);
//	}

}
